﻿/* ----------------------------------------------------------------------------
 * Copyright (c) Philip Stears 
 *
 * This source code is subject to terms and conditions of the Microsoft Public 
 * License (Ms-PL).
 * 
 * By using this source code in any fashion, you are agreeing to be bound 
 * by the terms of the Microsoft Public License.
 * 
 * To request a copy of the Microsoft Public License, send a request by email
 * to license@connectbasic.net.
 *
 * This notice must not be removed from this software.
 * ---------------------------------------------------------------------------- */

namespace ConnectBasic.Execution
{
    using System;
    using System.Reflection;
    using Microsoft.Linq.Expressions;
    using Microsoft.Scripting;
    
    /// <summary>
    /// Represents a function.
    /// </summary>
    public sealed class BasicFunction : IDynamicMetaObjectProvider
    {
        private string mName;
        private Delegate mTarget;
        private Delegate mTargetCallable;
        private BasicModule mOwner;

        internal BasicFunction(string name, Delegate target)
        {
            mName = name;
            mTarget = target;
        }

        #region Internal Methods

        internal void SetOwner(BasicModule owner)
        {
            if (mOwner != null) throw new InvalidOperationException();
            mOwner = owner;
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets the module which owns the method.
        /// </summary>
        public BasicModule Owner
        {
            get { return mOwner; }
        }

        /// <summary>
        /// Gets the name of the function.
        /// </summary>
        public string Name
        {
            get { return mName; }
        }

        /// <summary>
        /// Gets the return type of the function.
        /// </summary>
        public Type ReturnType
        {
            get { return mTarget.Method.ReturnType; }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Gets the number of parameters taken by the method.
        /// </summary>
        /// <returns></returns>
        public int GetParameterCount()
        {
            // First is the closure, second is the "this" parameter
            return mTarget.Method.GetParameters().Length - 2;
        }

        /// <summary>
        /// Gets a callable delegate.
        /// </summary>
        /// <returns></returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification="Too much work is done here to consider making it a property.")]
        public Delegate GetCallableDelegate()
        {
            if (mTargetCallable == null)
            {
                ParameterInfo[] pis = mTarget.Method.GetParameters();
                ParameterExpression[] outerParams = new ParameterExpression[pis.Length - 2];
                Expression[] innerArgs = new Expression[pis.Length - 1];

                // Ignore the first and second parameters (the first is a closure, the second is 
                // our "this" parameter
                innerArgs[0] = Expression.Constant(mOwner);

                for (int i = 2; i < pis.Length; i++)
                {
                    var outerParam = Expression.Parameter(pis[i].ParameterType, "$arg" + (i -2).ToString());

                    outerParams[i - 2] = outerParam;
                    innerArgs[i - 1] = outerParam;
                }

                // Create a lambda that calls the target with the "this" param already specified
                var callable =
                    Expression.Lambda(
                            Expression.Call(Expression.Constant(mTarget),
                                            mTarget.GetType().GetMethod("Invoke"),
                                            innerArgs
                            ),
                            outerParams);
                mTargetCallable = callable.Compile();
            }

            return mTargetCallable;
        }

        #endregion

        #region IDynamicObject Members

        DynamicMetaObject IDynamicMetaObjectProvider.GetMetaObject(Expression parameter)
        {
            return new BasicFunctionMetaObject(parameter, this);
        }

        #endregion

        #region BasicFunctionMetaObject

        private class BasicFunctionMetaObject : DynamicMetaObject
        {
            private BasicFunction mValueTyped;

            public BasicFunctionMetaObject(Expression parameter, BasicFunction value)
                : base(parameter, BindingRestrictions.Empty, value)
            {
                mValueTyped = value;
            }

            public override DynamicMetaObject BindInvoke(InvokeBinder binder, DynamicMetaObject[] args)
            {

                // Create the arguments to the delegate
                Expression[] fullArgs = new Expression[args.Length + 1];

                fullArgs[0] = Expression.Constant(mValueTyped.Owner); // "this"

                for (int i = 0; i < args.Length; i++)
                {
                    fullArgs[i + 1] = args[i].Expression;
                }

                // Return a meta object which calls invoke on the delegate
                return new DynamicMetaObject
                (
                    Expression.Call(
                        Expression.Constant(mValueTyped.mTarget),
                        mValueTyped.mTarget.GetType().GetMethod("Invoke"),
                        fullArgs
                    ),
                    BindingRestrictions.GetInstanceRestriction(this.Expression, this.Value)
                );
            }
        }

        #endregion

    }
}
